package cn.webestar.scms.security;

import cn.hutool.jwt.JWT;
import cn.webestar.scms.commons.Assert;
import cn.webestar.scms.commons.CommonException;
import cn.webestar.scms.commons.SysCode;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.AntPathMatcher;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author zgs
 */
@Slf4j
public class SessionFilter implements Filter {

    public final static List<String> FILTER_URLS = List.of("/api/**");
    private final static String ERROR_RESULT = "{ \"code\": %d, \"message\": \"%s\" }";
    private final AntPathMatcher matcher = new AntPathMatcher();

    private final RedisTemplate<String, Object> redisTemplate;

    private final SecurityProperties properties;

    public SessionFilter(RedisTemplate<String, Object> redisTemplate, SecurityProperties properties) {
        this.redisTemplate = redisTemplate;
        this.properties = properties;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        try {
            doIt(request, response, chain);
        } catch (Exception e) {
            printError(request, response, e);
        }
    }

    private void doIt(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {

        String cid = request.getHeader("_CID_");
//        if(StringUtils.hasText(cid)){
//            RequestContext context = new RequestContext();
//            context.setCid(Long.parseLong(cid));
//            ApiRequestContextHolder.setContext(context);
//        } else {
//            ApiRequestContextHolder.setContext(null);
//        }

        if (Constant.IGNORE_METHOD.equalsIgnoreCase(request.getMethod())) {
            chain.doFilter(request, response);
        } else if (matcherUrl(request.getRequestURI())) {
            SessionContext context = getSessionContext(request);
            Assert.notNull(context, SysCode.FORBIDDEN.getCode(), "request fail");
            SessionContextHolder.setContext(context);
            chain.doFilter(request, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    private SessionContext getSessionContext(HttpServletRequest request) throws Exception {

        String authHeader = request.getHeader(Constant.TOKEN_HEADER);
        Assert.notNull(authHeader, SysCode.FORBIDDEN.getCode(), "miss token");

        authHeader = URLDecoder.decode(authHeader, StandardCharsets.UTF_8);
        Assert.isTrue(authHeader.startsWith(Constant.TOKEN_PREFIX), SysCode.FORBIDDEN.getCode(), "miss bearer");

        String accessToken = authHeader.substring(Constant.TOKEN_PREFIX.length()).trim();
        JWT jwt = null;
        try {
            jwt = JwtUtils.parseToken(accessToken);
        } catch (Exception e) {
            throw new CommonException(SysCode.FORBIDDEN.getCode(), "invalid token");
        }
        Assert.notNull(jwt, SysCode.FORBIDDEN.getCode(), "invalid token");

        String id = (String) jwt.getPayload(JwtUtils.SUB_KEY);
        Assert.notNull(id, SysCode.FORBIDDEN.getCode(), "invalid token");

        // 因为accessToken会延期，所以这里不能校验jwt的过期参数
        // LocalDateTime expiresAt = (LocalDateTime) jwt.getPayload(JWTPayload.EXPIRES_AT);
        /// Assert.isTrue(expiresAt!=null && LocalDateTime.now().isAfter(expiresAt), SysCode.FORBIDDEN.getCode(), "expired token");

        String accessTokenKey = String.format(Constant.ACCESS_TOKEN_CACHE_KEY, properties.getCachePrefix(), id, accessToken);
        SessionContext context = (SessionContext) redisTemplate.opsForValue().get(accessTokenKey);
        Assert.notNull(context, SysCode.FORBIDDEN.getCode(), "invalid token");

        // accessToken 延期
        redisTemplate.expire(accessTokenKey, properties.getAccessTokenExpireIn(), TimeUnit.SECONDS);

        // refreshToken 延期
        String refreshTokenKey = String.format(Constant.REFRESH_TOKEN_CACHE_KEY, properties.getCachePrefix(), id, context.getRefreshToken());
        redisTemplate.expire(refreshTokenKey, properties.getAccessTokenExpireIn() * 2, TimeUnit.SECONDS);

        return context;
    }

    public boolean matcherUrl(String uri) {
        for (String includeUrl : FILTER_URLS) {
            if (matcher.match(includeUrl, uri)) {
                return true;
            }
        }
        return false;
    }

    private void printError(HttpServletRequest request, HttpServletResponse response, Exception e) {

        log.error(e.getMessage(), e);
        Integer code = SysCode.ERROR.getCode();
        String msg;

        if (e instanceof CommonException ce) {
            code = ce.getCode();
            msg = ce.getMessage();
        } else if (e.getCause() != null && e.getCause() instanceof CommonException ce) {
            code = ce.getCode();
            msg = ce.getMessage();
        } else {
            msg = "系统异常";
        }

        response.setContentType("application/json;charset=utf-8");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Headers", "*");
        if (SysCode.FORBIDDEN.getCode().equals(code)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        } else {
            response.setStatus(HttpServletResponse.SC_OK);
        }

        PrintWriter out = null;
        try {
            out = response.getWriter();
            out.write(String.format(ERROR_RESULT, code, msg));
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (out != null) {
                out.flush();
                out.close();
            }
        }

    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

}
